﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Cubicle.Core.DataContracts;
using Cubicle.Core.Interfaces;
using Mono.Cecil;

namespace Cubicle.Core.Shared
{
    public class FileResolver : IResolver
    {
        // Nlog
        private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();

        public const string AutoAssemblyResolveKey = "autoAssemblyResolve";
        public const string AutoTypeResolveKey = "autoTypeResolve";

        /// <summary>
        /// List of paths in cache
        /// </summary>
        private List<string> _paths = new List<string>();

        public bool AutoAssemblyResolveState { get; set; }
        public bool AutoTypeResolveState { get; set; }

        public FileResolver(bool autoAssemblyResolve = true)
        {
            AppDomain.CurrentDomain.AssemblyResolve += AutoAssemblyResolve;
            AutoAssemblyResolveState = autoAssemblyResolve;
            AutoTypeResolveState = false;
        }

        private Assembly AutoAssemblyResolve(object sender, ResolveEventArgs args)
        {
            return AutoAssemblyResolveState 
                ? ResolveAssembly(args.Name) 
                : null;
        }

        #region Implementation of IAssemblyFileCache

        public void AddAssemblyPath(string path)
        {
            if (!Directory.Exists(path))
            {
                _logger.Error("Failed to add assembly folder to resolver. Could not find assembly path. FolderPath={0}",
                          path);
                return;
            }

            _logger.Info("Adding path to file resolver. Directory = " + path);
            _paths.Add(path);
        }

        private IEnumerable<string> ListFiles(string path)
        {
            var files = Directory.GetFiles(path).Where(p => Path.GetExtension(p) == ".dll"
                                                             || Path.GetExtension(p) == ".exe");
            return files.Count() == 0
                       ? new List<string>()
                       : files.ToList();
        }

        private static Dictionary<string, AssemblyDefinition> ReadPathContents(string path)
        {
            var contents = new Dictionary<string, AssemblyDefinition>();
            foreach (var file in Helpers.GetDlls(path))
            {
                try
                {
                    var asmStream = new MemoryStream(File.ReadAllBytes(file));
                    var manifest = AssemblyDefinition.ReadAssembly(asmStream);
                    contents[Path.Combine(path, file)] = manifest;
                }
                catch (Exception e)
                {
                    _logger.ErrorException(e, "Already loaded or not an assembly. FileName = " + file);
                }
            }
            return contents;
        }

        public void RemoveAssemblyPath(string path)
        {
            _paths.Remove(path);
        }

        #endregion

        #region Implementation of IResolver

        public Assembly ResolveAssembly(string asmId)
        {
            // Assume asmId is a path
            var path = AssemblyIdToPath(asmId);
            if (path == null) return null;

            try
            {
                var asm = Assembly.LoadFrom(path);
                return asm;
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Could not resolve registered assembly. Path = {0}", path);
                return null;
            }           
        }

        private string AssemblyIdToPath(string asmId)
        {
            // Assume asmId is a path
            var path = asmId;
            if (!File.Exists(path))
                // If it's not a path, check if it's a FullName
                path = ResolveFromFullName(path);
            return path;
        }

        private string ResolveFromFullName(string fullName)
        {
            var matches = _paths
                .SelectMany(x => ReadPathContents(x))
                .Where(x => x.Value.FullName == fullName);

            if (matches.Count() == 0) return null;
            if (matches.Count() > 0)
            {
                _logger.Warn("Multiple matches found for assembly resolution. FullName = {0}", fullName);

                foreach (var m in matches)
                    _logger.Warn("Match: {0}", m.Key);
            }

            return matches.FirstOrDefault().Key;
        }

        public bool CanResolve(string asmId)
        {
            // Check if asmId is a fileName
            if (File.Exists(asmId))
            {
                try
                {
                    var bytes = Helpers.ReadBytesFromFile(asmId);
                    var def = Helpers.ReadCecilDefinition(bytes);
                    return true;
                } catch (Exception)
                {
                    return false;
                }
            }
            
            // Check if asmId is a FullName
            return AssemblyIdToPath(asmId) != null;
        }

        public IEnumerable<AssemblyData> ListAssemblies()
        {
            return _paths
                .SelectMany(x => ReadPathContents(x).Select(a => new AssemblyData(a.Value, a.Key)))
                .ToList();
        }

        public IEnumerable<TypeData> ListTypes(string asmId)
        {
            try
            {
                var path = AssemblyIdToPath(asmId);
                var bytes = Helpers.ReadBytesFromFile(path);
                return Helpers.ReadCecilTypes(bytes)
                    .Select(x => new TypeData(x, path))
                    .ToList();
            } catch (Exception) {}
            return new List<TypeData>();
        }

        public IEnumerable<TypeData> FindAll<TInterface>()
        {
            return _paths
                .SelectMany(ListTypes)
                .Where(x => x.InterfaceNames.Contains(typeof (TInterface).FullName));
        }

        public IEnumerable<TypeData> FindAll(string interfaceName)
        {
            return _paths
                .SelectMany(ListTypes)
                .Where(x => x.InterfaceNames.Contains(interfaceName));
        }

        public IEnumerable<Type> FindTypes<TInterface>(IEnumerable<string> typeNames)
        {
            var results = new List<Type>();
            foreach (var t in typeNames)
            {
                var type = GetType<TInterface>(t);
                results.Add(type);
            }
            return results;
        }

        public IEnumerable<Type> FindTypes<TInterface>()
        {
            var results = _paths
                .SelectMany(ListFiles)
                .SelectMany(ListTypes)
                .Where(t => t.InterfaceNames.Contains(typeof (TInterface).FullName))
                .Select(t => GetType<TInterface>(t.FullName))
                .ToList();
            return results;
        }

        private Type GetType<TInterface>(string fullname)
        {
            var type = Type.GetType(fullname);

            // If the type cannot be found, try to load it from disk
            if (type == null)
            {
                var files = _paths.SelectMany(ListFiles);
                var types = files.SelectMany(ListTypes);
                var match = types
                    .Where(x => x.FullName == fullname).FirstOrDefault();

                if (match != null && match.Path != null)
                    Assembly.LoadFrom(match.Path);
                type = Type.GetType(fullname);
            }

            if (type == null) return null;
            if (!type.GetInterfaces().Contains(typeof(TInterface))) return null;
            return type;
        }

        #endregion
    }
}
